package com.zdy.im.redis;

import com.sixeco.framework.core.redis.cluster.RedisClusterInterface;
import com.sixeco.framework.util.date.DateUtil;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;
import redis.clients.jedis.ScanResult;

import java.util.*;

/**
 * <p>
 * @deprecated See the <a href="https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide">OAuth 2.0 Migration Guide</a> for Spring Security 5.
 *
 * @Author 作者 : zdy
 * @CreateDate 创建时间: 2021-02-30
 */

public class RedisCacheTokenStore implements TokenStore {

	private static final String ACCESS = "access:";
	private static final String AUTH_TO_ACCESS = "auth_to_access:";
	private static final String AUTH = "auth:";
	private static final String REFRESH_AUTH = "refresh_auth:";
	private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
	private static final String REFRESH = "refresh:";
	private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
	private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
	private static final String UNAME_TO_ACCESS = "uname_to_access:";


	private final RedisClusterInterface connectionFactory;
	private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
	private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();

	private String prefix = "";


	public RedisCacheTokenStore(RedisClusterInterface connectionFactory) {
		this.connectionFactory = connectionFactory;

	}

	public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
		this.authenticationKeyGenerator = authenticationKeyGenerator;
	}

	public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
		this.serializationStrategy = serializationStrategy;
	}

	public void setPrefix(String prefix) {
		this.prefix = prefix;
	}



//	private RedisConnection getConnection() {
//		return connectionFactory.getConnection();
//	}

	private byte[] serialize(Object object) {
		return serializationStrategy.serialize(object);
	}

	private byte[] serializeKey(String object) {
		return serialize(prefix + object);
	}

	private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
		return serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
	}

	private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
		return serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
	}

	private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
		return serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
	}

	private byte[] serialize(String string) {
		return serializationStrategy.serialize(string);
	}

	private String deserializeString(byte[] bytes) {
		return serializationStrategy.deserializeString(bytes);
	}

	@Override
	public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
		String key = authenticationKeyGenerator.extractKey(authentication);
		byte[] serializedKey = serializeKey(AUTH_TO_ACCESS + key);
		byte[] bytes = null;
		try {
			bytes =  connectionFactory.get(serializedKey);
		} finally {
		}
		OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
		if (accessToken != null) {
			OAuth2Authentication storedAuthentication = readAuthentication(accessToken.getValue());
			if ((storedAuthentication == null || !key.equals(authenticationKeyGenerator.extractKey(storedAuthentication)))) {
				// Keep the stores consistent (maybe the same user is
				// represented by this authentication but the details have
				// changed)
				storeAccessToken(accessToken, authentication);
			}

		}
		return accessToken;
	}

	@Override
	public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
		return readAuthentication(token.getValue());
	}

	@Override
	public OAuth2Authentication readAuthentication(String token) {
		byte[] bytes = null;
		try {
			bytes = connectionFactory.get(serializeKey(AUTH + token));
		} finally {
		}
		OAuth2Authentication auth = deserializeAuthentication(bytes);
		return auth;
	}

	@Override
	public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
		return readAuthenticationForRefreshToken(token.getValue());
	}

	public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
		try {
			byte[] bytes = connectionFactory.get(serializeKey(REFRESH_AUTH + token));
			OAuth2Authentication auth = deserializeAuthentication(bytes);
			return auth;
		} finally {
		}
	}

	@Override
	public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
		byte[] serializedAccessToken = serialize(token);
		byte[] serializedAuth = serialize(authentication);
		byte[] accessKey = serializeKey(ACCESS + token.getValue());
		byte[] authKey = serializeKey(AUTH + token.getValue());
		byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
		byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
		byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());

		RedisClusterInterface conn = connectionFactory;
		Map<String,byte[]> hashKey = new HashMap<>(16);
		try {
//				conn.set(accessKey, serializedAccessToken);
//				conn.set(authKey, serializedAuth);
//				conn.set(authToAccessKey, serializedAccessToken);

			hashKey.put(new String(accessKey), serializedAccessToken);
			hashKey.put(new String(authKey), serializedAuth);
			hashKey.put(new String(authToAccessKey), serializedAccessToken);

			}catch (Exception e){
			e.printStackTrace();
			}
			if (!authentication.isClientOnly()) {
				conn.sadd(approvalKey, serializedAccessToken);
			}
			conn.sadd(clientId, serializedAccessToken);
		    int seconds = DateUtil.addField(new Date(), DateUtil.DateEnum.DAYS,1).getSeconds();
			if (token.getExpiration() != null && token.getExpiresIn() >0) {
				seconds = token.getExpiresIn();
				conn.expire(clientId, seconds);
				conn.expire(approvalKey, seconds);
			}
			connectionFactory.refinedMset(hashKey,seconds);

			OAuth2RefreshToken refreshToken = token.getRefreshToken();
			if (refreshToken != null && refreshToken.getValue() != null) {
				byte[] refresh = serialize(token.getRefreshToken().getValue());
				byte[] auth = serialize(token.getValue());
				byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
				byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
				Map<String,byte[]> key = new HashMap<>(16);
				key.put(new String(refreshToAccessKey), auth);
				key.put(new String(accessToRefreshKey), refresh);

				boolean expire = false;
				if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
					ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
					Date expiration = expiringRefreshToken.getExpiration();
					if (expiration != null) {
						seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
								.intValue();
						expire =true;
					}
				}
				if(expire){
					connectionFactory.refinedMset(hashKey,seconds);
				}else {
					connectionFactory.refinedMset(hashKey);
				}
			}
	}

	private static String getApprovalKey(OAuth2Authentication authentication) {
		String userName = authentication.getUserAuthentication() == null ? ""
				: authentication.getUserAuthentication().getName();
		return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
	}

	private static String getApprovalKey(String clientId, String userName) {
		return clientId + (userName == null ? "" : ":" + userName);
	}

	@Override
	public void removeAccessToken(OAuth2AccessToken accessToken) {
		removeAccessToken(accessToken.getValue());
	}

	@Override
	public OAuth2AccessToken readAccessToken(String tokenValue) {
		byte[] key = serializeKey(ACCESS + tokenValue);
		byte[] bytes = null;
		try {
			bytes = connectionFactory.get(key);
		} finally {
		}
		OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
		return accessToken;
	}

	public void removeAccessToken(String tokenValue) {
		String accessKey = new String(serializeKey(ACCESS + tokenValue));
		String authKey = new String(serializeKey(AUTH + tokenValue));
		String accessToRefreshKey = new String(serializeKey(ACCESS_TO_REFRESH + tokenValue));
		try {
//			conn.openPipeline();
//			conn.get(accessKey);
//			conn.get(authKey);
			Map<String, byte[]> results = connectionFactory.refinedMget(Arrays.asList(accessKey,authKey));
			connectionFactory.refinedDel(Arrays.asList(accessKey,accessToRefreshKey,authKey));
//			conn.del(accessKey);
//			conn.del(accessToRefreshKey);
			// Don't remove the refresh token - it's up to the caller to do that
//			conn.del(authKey);
			byte[] access =  results.get(accessKey);
			byte[] auth =  results.get(authKey);

			OAuth2Authentication authentication = deserializeAuthentication(auth);
			if (authentication != null) {
				String key = authenticationKeyGenerator.extractKey(authentication);
				byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
				byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
				byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
//				conn.openPipeline();
//				conn.del(authToAccessKey);
//				conn.sRem(unameKey, access);
//				conn.sRem(clientId, access);
//				conn.del(serialize(ACCESS + key));
//				conn.closePipeline();
				String authToAccessStr = new String(authToAccessKey);
				String accessKeyStr = new String(serialize(ACCESS + key));
				connectionFactory.srem(unameKey, access);
				connectionFactory.srem(clientId, access);
				connectionFactory.refinedDel(Arrays.asList(authToAccessStr,accessKeyStr ));
			}
		} finally {
		}
	}

	@Override
	public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
		byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
		byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
		byte[] serializedRefreshToken = serialize(refreshToken);
		Map<String,byte[]> key = new HashMap<>(16);
		key.put(new String(refreshKey), serializedRefreshToken);
		key.put(new String(refreshAuthKey), serialize(authentication));

		boolean expire = false;
		int seconds = 0;
		try {
			if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
				ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
				Date expiration = expiringRefreshToken.getExpiration();
				if (expiration != null) {
					seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
							.intValue();
					expire =true;

				}
			}
			if(expire){
				connectionFactory.refinedMset(key,seconds);
			}else {
				connectionFactory.refinedMset(key);
			}
		} finally {
		}
	}

	@Override
	public OAuth2RefreshToken readRefreshToken(String tokenValue) {
		byte[] key = serializeKey(REFRESH + tokenValue);
		byte[] bytes = null;
		try {
			bytes = connectionFactory.get(key);
		} finally {
		}
		OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
		return refreshToken;
	}

	@Override
	public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
		removeRefreshToken(refreshToken.getValue());
	}

	public void removeRefreshToken(String tokenValue) {
		String refreshKey = new String(serializeKey(REFRESH + tokenValue));
		String refreshAuthKey = new String(serializeKey(REFRESH_AUTH + tokenValue));
		String refresh2AccessKey = new String(serializeKey(REFRESH_TO_ACCESS + tokenValue));
		String access2RefreshKey = new String(serializeKey(ACCESS_TO_REFRESH + tokenValue));
		try {
			List<String> listKey = Arrays.asList(refreshKey,refreshAuthKey,refresh2AccessKey,access2RefreshKey);
			connectionFactory.refinedDel(listKey);
		} finally {
		}
	}

	@Override
	public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
		removeAccessTokenUsingRefreshToken(refreshToken.getValue());
	}

	private void removeAccessTokenUsingRefreshToken(String refreshToken) {
		byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
		byte[] bytes = connectionFactory.get(key);
		connectionFactory.del(key);
		if (bytes == null) {
			return;
		}
		String accessToken = deserializeString(bytes);
		if (accessToken != null) {
			removeAccessToken(accessToken);
		}
	}

	private List<byte[]> getByteLists(byte[] approvalKey) {
		byte[]  cursorParam = "0".getBytes();
		ScanResult<byte[]> result = connectionFactory.sscan(approvalKey, cursorParam);
		if(null != result){
			return result.getResult();
		}
		return null;

	}

	@Override
	public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
		byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
		List<byte[]> byteList = getByteLists(approvalKey);
		if (byteList == null || byteList.size() == 0) {
			return Collections.<OAuth2AccessToken> emptySet();
		}
		List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
	}

	@Override
	public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
		byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
		List<byte[]> byteList = getByteLists(key);

		if (byteList == null || byteList.size() == 0) {
			return Collections.<OAuth2AccessToken> emptySet();
		}
		List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
		for (byte[] bytes : byteList) {
			OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
			accessTokens.add(accessToken);
		}
		return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
	}

}
